using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using com.bodurov.SilverlightControls.XmlCodeEditor.Enums;
using com.bodurov.SilverlightControls.XmlCodeEditor.Extensions;

namespace com.bodurov.SilverlightControls.XmlCodeEditor
{
    public class SplitTokenizer : ITokenizer
    {
        private static readonly Regex _regex_SplitLines = new Regex("(\r\n|\n\r|\r|\n)", RegexOptions.ExplicitCapture);
        private static readonly HashSet<string> _keywords = new HashSet<string>(new[] { "Context", "Html", "Response", "Request", "abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", "class", "const", "continue", "decimal", "default", "delegate", "do", "double", "else", "enum", "event", "explicit", "extern", "false", "finally", "fixed", "float", "for", "foreach", "goto", "if", "implicit", "in", "int", "interface", "internal", "is", "lock", "long", "namespace", "new", "null", "object", "operator", "out", "override", "params", "private", "protected", "public", "readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof", "stackalloc", "static", "string", "struct", "switch", "this", "throw", "true", "try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using", "virtual", "volatile", "void", "while" });
        private static readonly char[] _tagNameDelimiters = new[] { ' ', '\t', '\r', '\n', '>', '/' };
        private static readonly char[] _keyWordDelimiters = new[] { ' ', '\t', '\r', '\n', '>', '=', '.', ',', ':', ';', '!', '<', '{', '}', '(', ')', '[', ']', '+', '-', '*', '/', '%' };
        private static readonly char[] _colon = new[] { ':' };
        private static readonly string[] _serverScriptTerminators = new[] { "%>" };
        private static readonly string[] _tagTerminators = new[] { "/>", ">" };
        private static readonly string[] _processingInstructionTerminators = new[] { "/>", "?>", ">" };

        public List<ListOfTokens> ParseTokens(string value)
        {
            return ParseTokens(value, XmlArea.OutsideTag);
        }

        public List<ListOfTokens> ParseTokens(string value, XmlArea area)
        {
            var lines = _regex_SplitLines.Split(value);
            var list = new List<ListOfTokens>();
            lines.Aggregate(area, (xmlarea, line) => ParseTokensInsiteSingleLine(line, xmlarea, list));
            return list;
        }

        private static XmlArea ParseTokensInsiteSingleLine(string line, XmlArea area, List<ListOfTokens> list)
        {
            var lineOfTokens = new ListOfTokens();
            list.Add(lineOfTokens);
            return ParseTokensInsiteSingleLine(line, area, lineOfTokens, true);
        }
        private static XmlArea ParseTokensInsiteSingleLine(string line, XmlArea area, ListOfTokens lineOfTokens, bool ensureLineLength)
        {
            if (String.IsNullOrEmpty(line))
            {
                lineOfTokens.AddLast(new Token { Content = line, Within = area, Type = XmlElement.WhiteSpace });
                return area;
            }
            var expectedLength = line.Length;

            var startTagStrings = line.Split('<');
            for (var i = 0; i < startTagStrings.Length; i++)
            {
                var current = startTagStrings[i];
                if (i > 0) current = String.Concat("<", current);
                if (current == "") continue;
                if (area == XmlArea.InsideClientSideComment)
                {
                    area = ParseTagForCompletion(current, lineOfTokens, area, XmlElement.InsideClientSideComment, XmlElement.ClosingClientSideCommentBracket, "-->", false);
                    continue;
                }
                if (area == XmlArea.InsideServerScriptCode || area == XmlArea.InsideServerScriptPrintCode || area == XmlArea.InsideServerBindStatement)
                {
                    area = ParseTagForCompletion(current, lineOfTokens, area, XmlElement.InsideServerScript, XmlElement.ClosingServerScriptBracket, "%>", true);
                    continue;
                }
                if (area == XmlArea.InsideServerDeclaration)
                {
                    area = SplitAttributesAndLookForEnd(lineOfTokens, current, area, _serverScriptTerminators, XmlElement.ClosingServerScriptBracket);
                    continue;
                }
                if (area == XmlArea.InsideProcessingInstruction)
                {
                    area = SplitAttributesAndLookForEnd(lineOfTokens, current, area, _processingInstructionTerminators, XmlElement.ClosingProcessingInstructionBracket);
                    continue;
                }
                if (area == XmlArea.InsideTag && !current.StartsWith("<%"))
                {
                    area = SplitAttributesAndLookForEnd(lineOfTokens, current, area, _tagTerminators, XmlElement.OpeningTagEndBracket);
                    continue;
                }

                if (i > 0)
                {
                    if (IsNotRealNewTag(current))
                    {
                        AddToken(lineOfTokens, current, area = XmlArea.OutsideTag, XmlElement.OutsideTagContent);
                    }
                    else
                    {
                        area = ParseTag(current, lineOfTokens);
                    }
                }
                else
                {
                    AddToken(lineOfTokens, current, area = XmlArea.OutsideTag, XmlElement.OutsideTagContent);
                }
            }
            if (ensureLineLength && expectedLength != lineOfTokens.StringLength)
            {
                lineOfTokens.Clear();
                AddToken(lineOfTokens, line, area = XmlArea.OutsideTag, XmlElement.OutsideTagContent);
            }
            return area;
        }

        private static bool IsNotRealNewTag(string current)
        {
            return current.Length > 1 && (Char.IsWhiteSpace(current[1]) || current[1] == '=');
        }

        private static XmlArea ParseTagForCompletion(string str, ListOfTokens list, XmlArea area, XmlElement elem, XmlElement term, string terminator, bool parseKeywords)
        {
            var parts = str.Split(new[] { terminator }, StringSplitOptions.None).SetMaxElements(2);

            if (parseKeywords) { ParseKeywords(list, parts[0], area, elem); }
            else{ AddToken(list, parts[0], area, elem); }
            if(parts.Length > 1)
            {
                AddToken(list, terminator, area, term);
                area = ParseTokensInsiteSingleLine(parts[1], XmlArea.OutsideTag, list, false);
            }
            return area;
        }

        private static void ParseKeywords(ListOfTokens list, string s, XmlArea area, XmlElement elem)
        {
            var sb = new StringBuilder();
            var arr = s.SplitAndRetainDelimiter(_keyWordDelimiters);
            for(var i = 0; i < arr.Length; i++)
            {
                var curr = arr[i];
                if (_keywords.Contains(curr))
                {
                    if(sb.Length > 0)
                    {
                        AddToken(list, sb.ToString(), area, elem);
                        sb.Clear();
                    } 
                    AddToken(list, curr, area, XmlElement.Keyword);
                }
                else
                {
                    sb.Append(curr);
                }
            }
            if (sb.Length > 0)
            {
                AddToken(list, sb.ToString(), area, elem);
            } 
        }


        private static XmlArea ParseTag(string startTagString, ListOfTokens list)
        {
            string remainder;
            string[] terminators;
            XmlArea area;
            XmlElement terminatorName;
            if(startTagString.StartsWith("<%="))
            {
                area = XmlArea.InsideServerScriptPrintCode;
                AddToken(list, "<%=", area, XmlElement.OpeningServerScriptBracket);
                remainder = startTagString.Substring(3);
                return ParseTagForCompletion(remainder, list, area, XmlElement.InsideServerScript, XmlElement.ClosingServerScriptBracket, "%>", true);
            }
            else if (startTagString.StartsWith("<%@"))
            {
                area = XmlArea.InsideServerDeclaration;
                AddToken(list, "<%@", area, XmlElement.OpeningServerScriptBracket);
                terminators = _serverScriptTerminators;
                remainder = startTagString.Substring(3);
                terminatorName = XmlElement.ClosingServerScriptBracket;
            }
            else if (startTagString.StartsWith("<%#"))
            {
                area = XmlArea.InsideServerBindStatement;
                AddToken(list, "<%#", area, XmlElement.OpeningServerScriptBracket);
                remainder = startTagString.Substring(3);
                return ParseTagForCompletion(remainder, list, area, XmlElement.InsideServerScript, XmlElement.ClosingServerScriptBracket, "%>", true);
            }
            else if (startTagString.StartsWith("<!--"))
            {
                area = XmlArea.InsideClientSideComment;
                AddToken(list, "<!--", area, XmlElement.OpeningClientSideCommentBracket);
                remainder = startTagString.Substring(4);
                return ParseTagForCompletion(remainder, list, area, XmlElement.InsideClientSideComment, XmlElement.ClosingClientSideCommentBracket, "-->", false);
            }
            else if (startTagString.StartsWith("<%"))
            {
                area = XmlArea.InsideServerScriptCode;
                AddToken(list, "<%", area, XmlElement.OpeningServerScriptBracket);
                remainder = startTagString.Substring(2);
                return ParseTagForCompletion(remainder, list, area, XmlElement.InsideServerScript, XmlElement.ClosingServerScriptBracket, "%>", true);
            }
            else if (startTagString.StartsWith("<?"))
            {
                area = XmlArea.InsideProcessingInstruction;
                AddToken(list, "<?", area, XmlElement.OpeningTagBeginBracket);
                terminators = _processingInstructionTerminators;
                remainder = startTagString.Substring(2);
                terminatorName = XmlElement.ClosingTagBeginBracket;
            }
            else if (startTagString.StartsWith("<"))
            {
                area = XmlArea.InsideTag;
                if (startTagString.StartsWith("</"))
                {
                    AddToken(list, "</", area, XmlElement.ClosingTagBeginBracket);
                    startTagString = startTagString.Substring(2);
                }
                else
                {
                    AddToken(list, "<", area, XmlElement.OpeningTagBeginBracket);
                    startTagString = startTagString.Substring(1);
                }

                var parts = startTagString.SplitAndRetainDelimiter(_tagNameDelimiters, 2);
                
                var name = parts[0];
                var nameParts = name.SplitAndRetainDelimiter(_colon, 2);
                if (nameParts.Length > 2)
                {
                    AddToken(list, nameParts[0] + nameParts[1], area, XmlElement.OpeningTagNamespace);
                    AddToken(list, nameParts[2], area, XmlElement.OpeningTagName);
                }
                else
                {
                    AddToken(list, name, area, XmlElement.OpeningTagName);
                }
                remainder = parts.Length == 1 ? "" : parts.Length == 2 ? parts[1] : parts[1] + parts[2];
                
                terminators = _tagTerminators;
                terminatorName = XmlElement.OpeningTagClosingBracket;
            }
            else
            {
                AddToken(list, startTagString, area = XmlArea.OutsideTag, XmlElement.OutsideTagContent);
                return area;
            }
            return SplitAttributesAndLookForEnd(list, remainder, area, terminators, terminatorName);
        }

        private static XmlArea SplitAttributesAndLookForEnd(ListOfTokens list, string html, XmlArea area, string[] terminators, XmlElement terminatorName)
        {
            string[] parts = null;
            string terminator = null;
            for(var i = 0; i < terminators.Length; i++)
            {
                parts = html.Split(new[] { terminator = terminators[i] }, StringSplitOptions.None).SetMaxElements(2);
                if (parts.Length > 1) break;
            }
            ParseAttributes(list, parts[0], area);
            if(parts.Length > 1)
            {
                AddToken(list, terminator, area, terminatorName);
                area = XmlArea.OutsideTag;
                if (parts[1] != "") AddToken(list, parts[1], area, XmlElement.OutsideTagContent);
            }
            return area;
        }

        private static void ParseAttributes(ListOfTokens list, string str, XmlArea area)
        {
            if (str == "") return;
            var parts = str.SplitQuotedStrings();
            for(var i = 0; i < parts.Length; i++)
            {
                var current = parts[i];
                if(current.StartsWith("\""))
                {
                    AddToken(list, current, area | XmlArea.InsideDualQuoteString, XmlElement.AttributeDualQuoteString);
                }
                else if (current.StartsWith("'"))
                {
                    AddToken(list, current, area | XmlArea.InsideSingleQuoteString, XmlElement.AttributeSingleQuoteString);
                }
                else
                {
                    ParseAttributeNames(list, current, area);
                }
            }
        }

        private static void ParseAttributeNames(ListOfTokens list, string str, XmlArea area)
        {
            XmlElement element = XmlElement.Unknown;
            var sb = new StringBuilder();
            for(var i = 0; i < str.Length; i++)
            {
                var ch = str[i];
                if(Char.IsWhiteSpace(ch))
                {
                    if (element != XmlElement.WhiteSpace && sb.Length > 0)
                    {
                        AddToken(list, sb.ToString(), area, element);
                        sb.Clear();
                    }
                    element = XmlElement.WhiteSpace;
                    sb.Append(ch);
                }
                else if (ch == '=')
                {
                    if (sb.Length > 0)
                    {
                        AddToken(list, sb.ToString(), area, element);
                        sb.Clear();
                    }
                    element = XmlElement.AttributeEqualSign;
                    AddToken(list, "=", area, element);
                }
                else
                {
                    if (element != XmlElement.AttributeName && sb.Length > 0)
                    {
                        AddToken(list, sb.ToString(), area, element);
                        sb.Clear();
                    }
                    element = XmlElement.AttributeName;
                    sb.Append(ch);
                }
            }
            if (sb.Length > 0)
            {
                AddToken(list, sb.ToString(), area, element);
            }
        }

        private static void AddToken(ListOfTokens list, string content, XmlArea area, XmlElement type)
        {
            list.StringLength += content.Length;
            list.AddLast(new Token { Content = content, Within = area, Type = type });
        }
    }
}